package org.datascooter.meta;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.CharBuffer;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;

import org.datascooter.exception.DataScooterException;
import org.datascooter.exception.SnipManagerException;
import org.datascooter.impl.Snip;
import org.datascooter.inface.IBackupManager;
import org.datascooter.inface.ISnipBuilder;
import org.datascooter.inface.ISnipManager;
import org.datascooter.utils.LangUtils;
import org.datascooter.utils.ProgressTask;
import org.datascooter.utils.SnipUtils;
import org.datascooter.utils.TypeUtils;
import org.datascooter.utils.policy.BackupPolicy;
import org.datascooter.utils.policy.RestorePolicy;
import org.datascooter.utils.policy.SnipType;

public class BackupManager implements IBackupManager {

	private static final String AFTER_SCRIPTS = "After Scripts";
	private static final String AFTER_FILES = "After Files";
	private static final String TABLES = "Tables";
	private static final String BEFORE_SKRIPTS = "Before Skripts";
	private static final String BEFORE_FILES = "Before Files";
	private static final int maxQueryLength = 40;
	private ISnipManager snipManager;
	private File[] beforeFiles = new File[0];
	private String[] beforeSkripts = new String[0];;
	private File[] afterFiles = new File[0];
	private String[] afterScripts = new String[0];
	private ProgressTask progressTask;
	private final ISnipBuilder snipBuilder;
	private Map<String, TableReplace> replaceMap = new HashMap<String, TableReplace>();

	public BackupManager(ISnipManager snipManager, ISnipBuilder builder) {
		this.snipManager = snipManager;
		this.snipBuilder = builder;
	}

	@Override
	public void setDataReplace(TableReplace... replaceArray) {
		if (replaceArray != null && replaceArray.length > 0) {
			for (TableReplace dataReplace : replaceArray) {
				replaceMap.put(dataReplace.tableName, dataReplace);
			}
		}
	}

	@Override
	public void setExecuteBefore(File... files) {
		this.beforeFiles = files;
	}

	@Override
	public void setExecuteBefore(String... scripts) {
		this.beforeSkripts = scripts;
	}

	@Override
	public void setExecuteAfter(File... files) {
		this.afterFiles = files;
	}

	@Override
	public void setExecuteAfter(String... scripts) {
		this.afterScripts = scripts;
	}

	@Override
	public void backup(String shemaName, String dirPath, String fileName, BackupPolicy backupPolicy, boolean include, String... tables) {
		try {
			preparePreProgress(fileName);
			File dir = new File(dirPath);
			if (!dir.exists() && !dir.mkdirs()) {
				throw new DataScooterException("***Unable to create file: " + dir.getPath());
			}
			File file = new File(dir, fileName);
			if (!file.exists() && !file.createNewFile()) {
				throw new DataScooterException("***Unable to create file: " + file.getPath());
			}
			log("***Backup starts - file : " + file.getPath() + "  at " + TypeUtils.formatDate(new Date()));
			executeBefore();
			List<MetaTable> sorted = LangUtils.sortTablesByReferences(wrapTables(shemaName, include, tables));
			prepareProgress(sorted);
			ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
			try {
				out.writeObject(new BackupData(sorted, snipManager.getCurrentDate()));
				for (MetaTable table : sorted) {
					startWork(TABLES, table.getName());
					MetaTable tableData = snipManager.getTableData(table);
					replaceData(tableData);
					switch (backupPolicy) {
						case ALL_TABLES:
							if (tableData.getData().size() > 0) {
								tableData.setData(LangUtils.sortRowsByReferences(tableData));
								out.writeObject(tableData);
								log("    Saved : " + tableData.getName() + "  rows: " + tableData.getData().size());
							} else {
								out.writeObject(tableData);
								log("    Saved : " + tableData.getName() + "  rows: " + tableData.getData().size());
							}
							break;
						case NOT_EMPTY_TABLES:
							if (tableData.getData().size() > 0) {
								tableData.setData(LangUtils.sortRowsByReferences(tableData));
								out.writeObject(tableData);
								log("    Saved : " + tableData.getName() + "  rows: " + tableData.getData().size());
							} else {
								log("    Saved : " + tableData.getName() + "  is empty ");
							}
							break;
					}
					doWork(TABLES, table.getName());
				}
			} finally {
				out.close();
			}
			executeAfter();
			log("***Backup complete -  tables: " + sorted.size() + "  file : " + file.getPath() + "  size: " + file.length() + "  at " + TypeUtils.formatDate(new Date()));
		} catch (IOException e) {
			e.printStackTrace();
			throw new DataScooterException(" -dirPath -" + dirPath + "    fileName - " + fileName, e);
		} catch (SnipManagerException e) {
			e.printStackTrace();
			throw new DataScooterException(" -dirPath -" + dirPath + "    fileName - " + fileName, e);
		}
	}

	private void replaceData(MetaTable tableData) {
		if (replaceMap.size() > 0) {
			TableReplace dataReplace = replaceMap.get(tableData.getName());
			if (dataReplace == null) {
				return;
			}
			ColumnReplace[] valuesArray = new ColumnReplace[tableData.getColumns().size()];
			for (int a = 0; a < tableData.getColumns().size(); a++) {
				MetaColumn column = tableData.getColumns().get(a);
				for (ColumnReplace value : dataReplace.values) {
					if (column.COLUMN_NAME.equalsIgnoreCase(value.columnName)) {
						valuesArray[a] = value;
					}
				}
			}
			int length = tableData.getColumns().size();
			ColumnReplace replaceValue = null;
			for (Object[] array : tableData.getData()) {
				for (int a = 0; a < length; a++) {
					replaceValue = valuesArray[a];
					if (replaceValue == null) {
						continue;
					}
					if (replaceValue.oldValue == null || replaceValue.constraint == null) {
						array[a] = replaceValue.newValue;
					} else {
						setData(replaceValue, array, a);
					}
				}
			}
		}
	}

	private void setData(ColumnReplace replaceValue, Object[] array, int a) {
		switch (replaceValue.constraint) {
			case EQUAL:
				if (LangUtils.equalsAnyWay(replaceValue.oldValue, array[a])) {
					array[a] = replaceValue.newValue;
				}
				break;
			case NOT_EQUAL:
				if (!LangUtils.equalsAnyWay(replaceValue.oldValue, array[a])) {
					array[a] = replaceValue.newValue;
				}
				break;
			case GREATER_THAN:
				if (LangUtils.compareAnyWay(replaceValue.oldValue, array[a]) > 0) {
					array[a] = replaceValue.newValue;
				}
				break;
			case GREATER_THAN_OR_EQUAL:
				if (LangUtils.compareAnyWay(replaceValue.oldValue, array[a]) >= 0) {
					array[a] = replaceValue.newValue;
				}
				break;
			case LESS_THAN:
				if (LangUtils.compareAnyWay(replaceValue.oldValue, array[a]) < 0) {
					array[a] = replaceValue.newValue;
				}
				break;
			case LESS_THAN_OR_EQUAL:
				if (LangUtils.compareAnyWay(replaceValue.oldValue, array[a]) <= 0) {
					array[a] = replaceValue.newValue;
				}
				break;
			case LIKE:
				if (LangUtils.like(array[a], replaceValue.oldValue)) {
					array[a] = replaceValue.newValue;
				}
				break;
			case NOT_LIKE:
				if (!LangUtils.like(array[a], replaceValue.oldValue)) {
					array[a] = replaceValue.newValue;
				}
				break;
		}
	}

	@Override
	public void backupToSql(String shemaName, String dirPath, String fileName, BackupPolicy backupPolicy, boolean include, String... tables) {
		try {
			preparePreProgress(fileName);
			File dir = new File(dirPath);
			if (!dir.exists() && !dir.mkdirs()) {
				throw new DataScooterException("***Unable to create file: " + dir.getPath());
			}
			File file = new File(dir, fileName);
			if (!file.exists() && !file.createNewFile()) {
				throw new DataScooterException("***Unable to create file: " + file.getPath());
			}
			log("***Backup to SQL starts - file : " + file.getPath() + "  at " + TypeUtils.formatDate(new Date()));
			executeBefore();
			List<MetaTable> sorted = LangUtils.sortTablesByReferences(wrapTables(shemaName, include, tables));
			prepareProgress(sorted);
			List<String> result = new ArrayList<String>();
			for (MetaTable table : sorted) {
				MetaTable tableData = snipManager.getTableData(table);
				if (tableData.getData().size() == 0) {
					continue;
				}
				StringBuilder columnBuilder = new StringBuilder();
				for (MetaColumn column : tableData.getColumns()) {
					columnBuilder.append((columnBuilder.length() > 0 ? " ," : "") + " " + column.COLUMN_NAME);
				}
				List<Object[]> sortedRows = LangUtils.sortRowsByReferences(table);
				for (Object[] array : sortedRows) {
					StringBuilder valuesBuilder = new StringBuilder();
					for (int a = 0; a < array.length; a++) {
						Object object = array[a];
						String valueString = "";
						if (object == null || object.toString().equalsIgnoreCase("null")) {
							valueString = (valuesBuilder.length() > 0 ? SnipUtils.COMMA : SnipUtils.NN) + SnipUtils.NULL;
						} else if (TypeUtils.isDBTypeNumeric(tableData.getColumns().get(a).TYPE_NAME)) {
							valueString = (valuesBuilder.length() > 0 ? SnipUtils.COMMA : SnipUtils.NN) + object;
						} else {
							valueString = (valuesBuilder.length() > 0 ? SnipUtils.COMMA : SnipUtils.NN) + "'" + object + "'";
						}
						valueString = valueString.replaceAll("\n", "  ").replaceAll(";", "  ");
						valuesBuilder.append(valueString);
					}
					result.add(MessageFormat.format(SnipUtils.INSERT_INTO, table.getName(), columnBuilder.toString(), valuesBuilder.toString()) + ";\n");
				}
				result.add("\n");
			}
			writeFile(file, result);
			executeAfter();
			log("***Backup to SQL complete -  tables: " + sorted.size() + "  file : " + file.getPath() + "  size: " + file.length() + "  at " + TypeUtils.formatDate(new Date()));
		} catch (IOException e) {
			throw new DataScooterException(" -dirPath -" + dirPath + "    fileName - " + fileName, e);
		} catch (SnipManagerException e) {
			throw new DataScooterException(" -dirPath -" + dirPath + "    fileName - " + fileName, e);
		}
	}

	@Override
	public void restore(String shemaName, String dirPath, String fileName, RestorePolicy restorePolicy, boolean include, String... tables) {
		File file = new File(dirPath, fileName);
		int count = 0;
		int countData = 0;
		if (file.exists()) {
			preparePreProgress(fileName);
			try {
				executeBefore();
				log("***Restore starts - file : " + file.getPath() + "  size: " + file.length() + "  at " + TypeUtils.formatDate(new Date()));
				FileInputStream fis = new FileInputStream(file);
				ObjectInputStream in = new ObjectInputStream(fis);
				try {
					try {
						BackupData data = (BackupData) in.readObject();
						prepareProgress(data.sorted);
						boolean wrap = false;
						if (tables.length > 0) {
							Arrays.sort(tables);
							wrap = true;
						}
						MetaTable table;
						int tabNumber = 0;
						startWork(TABLES, data.sorted.get(tabNumber).getName());
						while ((table = (MetaTable) in.readObject()) != null) {
							if (wrap) {
								if (include) {
									if (Arrays.binarySearch(tables, table.getName()) < 0) {
										continue;
									}
								} else {
									if (Arrays.binarySearch(tables, table.getName()) >= 0) {
										continue;
									}
								}
							}
							if (manageTable(shemaName, table, restorePolicy)) {
								count++;
								if (table.getData().size() > 0) {
									restoreTableData(shemaName, table);
									countData++;
								} else {
									log("    Restore : " + table.getName() + "  is empty ");
								}
							}
							doWork(TABLES, table.getName());
							++tabNumber;
							if (tabNumber < data.sorted.size())
								startWork(TABLES, data.sorted.get(tabNumber).getName());
						}
					} catch (EOFException e) {
					} catch (ClassNotFoundException e) {
						throw new DataScooterException(" -dirPath -" + dirPath + "    fileName - " + fileName, e);
					}
				} finally {
					in.close();
				}
				log("***Restore completed - tables / with  data : " + count + "/" + countData + "  file : " + file.getPath() + "  size: " + file.length() + "  at " + TypeUtils.formatDate(new Date()));
				executeAfter();
			} catch (IOException e) {
				throw new DataScooterException(" -dirPath -" + dirPath + "    fileName - " + fileName, e);
			} catch (SnipManagerException e) {
				throw new DataScooterException(" -dirPath -" + dirPath + "    fileName - " + fileName, e);
			}
		}
	}

	private void startWork(String... nameArray) {
		ProgressTask child = progressTask.getChild(nameArray);
		if (child != null) {
			child.start();
		}
	}

	private void doWork(String... nameArray) {
		ProgressTask child = progressTask.getChild(nameArray);
		if (child != null) {
			child.work();
		}
	}

	private void preparePreProgress(String fileName) {
		progressTask = new ProgressTask("Backup to file: " + fileName, "");
		if (beforeFiles != null && beforeFiles.length > 0) {
			ProgressTask createChild = progressTask.createChild(BEFORE_FILES, "", beforeFiles.length);
			for (File file : beforeFiles) {
				createChild.createChild(file.getName(), file.getPath(), 1);
			}
		}
		if (beforeSkripts != null && beforeSkripts.length > 0) {
			ProgressTask createChild = progressTask.createChild(BEFORE_SKRIPTS, "", beforeSkripts.length);
			for (String query : beforeSkripts) {
				createChild.createChild(shortQueryString(query), query, 1);
			}
		}
	}

	private void prepareProgress(List<MetaTable> sorted) {
		ProgressTask createChild = progressTask.createChild(TABLES, "", sorted.size());
		for (MetaTable table : sorted) {
			createChild.createChild(table.getName(), "", 1);
		}
		if (afterFiles != null && afterFiles.length > 0) {
			createChild = progressTask.createChild(AFTER_FILES, "", afterFiles.length);
			for (File file : afterFiles) {
				createChild.createChild(file.getName(), file.getPath(), 1);
			}
		}
		if (afterScripts != null && afterScripts.length > 0) {
			createChild = progressTask.createChild(AFTER_SCRIPTS, "", afterScripts.length);
			for (String query : afterScripts) {
				createChild.createChild(shortQueryString(query), query, 1);
			}
		}
	}

	private void executeBefore() throws FileNotFoundException, IOException, SnipManagerException {
		if (beforeFiles != null) {
			executeFiles(beforeFiles, true);
		}
		if (beforeSkripts != null) {
			executeScripts(beforeSkripts, true);
		}
	}

	private void executeAfter() throws IOException, SnipManagerException {
		if (afterFiles != null) {
			executeFiles(afterFiles, false);
		}
		if (afterScripts != null) {
			executeScripts(afterScripts, false);
		}
	}

	private void executeFiles(File[] array, boolean isBefore) throws FileNotFoundException, IOException, SnipManagerException {
		CharBuffer buffer = CharBuffer.allocate(10000);
		for (File file : array) {
			if (file.exists()) {
				if (isBefore) {
					startWork(BEFORE_FILES, file.getName());
					snipManager.execute(new Snip(null, SnipType.EXECUTE, readFile(buffer, file).toString(), null, null, null, -1, null));
					log("***Executed before file  : " + file.getPath() + "  at " + TypeUtils.formatDate(new Date()));
					doWork(BEFORE_FILES, file.getName());
				} else {
					startWork(AFTER_FILES, file.getName());
					snipManager.execute(new Snip(null, SnipType.EXECUTE, readFile(buffer, file).toString(), null, null, null, -1, null));
					log("***Executed after file  : " + file.getPath() + "  at " + TypeUtils.formatDate(new Date()));
					doWork(AFTER_FILES, file.getName());
				}
			} else {
				log("***File not found  : " + file.getPath());
			}
		}
	}

	private void executeScripts(String[] array, boolean isBefore) throws SnipManagerException {
		for (String query : array) {
			String string = shortQueryString(query);
			if (isBefore) {
				startWork(BEFORE_SKRIPTS, string);
				snipManager.execute(new Snip(null, SnipType.EXECUTE, query, null, null, null, -1, null));
				log("***Executed before script  : " + string + "  at " + TypeUtils.formatDate(new Date()));
				doWork(BEFORE_SKRIPTS, string);
			} else {
				startWork(AFTER_SCRIPTS, string);
				snipManager.execute(new Snip(null, SnipType.EXECUTE, query, null, null, null, -1, null));
				log("***Executed after script  : " + string + "  at " + TypeUtils.formatDate(new Date()));
				doWork(AFTER_SCRIPTS, string);
			}
		}
	}

	private String shortQueryString(String query) {
		String string = query.length() > maxQueryLength ? query.substring(0, maxQueryLength) + "..." : query;
		return string;
	}

	private StringBuilder readFile(CharBuffer buffer, File file) throws FileNotFoundException, IOException {
		StringBuilder builder = new StringBuilder();
		FileReader reader = new FileReader(file);
		buffer.clear();
		int a = 0;
		while ((a = reader.read(buffer)) > 0) {
			builder.append(buffer.array(), 0, a);
			buffer.clear();
		}
		reader.close();
		return builder;
	}

	private void writeFile(File file, List<String> result) throws IOException {
		FileWriter writer = new FileWriter(file);
		try {
			for (String stringValue : result) {
				writer.write(stringValue);
				writer.flush();
			}
		} finally {
			writer.close();
		}
	}

	private boolean manageTable(String shemaName, MetaTable table, RestorePolicy restorePolicy) throws SnipManagerException {
		switch (restorePolicy) {
			case ONLY_EXISTS_TABLES:
				if (snipManager.isTableExists(shemaName, table.getName())) {
					return true;
				} else {
					log("***Restore of table " + table.getName() + "  impossible  -  it is not exists");
				}
				break;
			case TRY_TO_CREATE:
				if (snipManager.isTableExists(shemaName, table.getName())) {
					return true;
				} else {
					createTable(table);
					return true;
				}
			case TRY_TO_CREATE_SILENT:
				if (snipManager.isTableExists(shemaName, table.getName())) {
					return true;
				} else {
					try {
						createTable(table);
						return true;
					} catch (Throwable e) {
						log("***Restore of table " + table.getName() + "  impossible  -  it is not exists and  might not be created");
						return false;
					}
				}
			case THROW_IF_NOT_TABLE:
				if (snipManager.isTableExists(shemaName, table.getName())) {
					return true;
				} else {
					throw new DataScooterException("*** ERROR table " + table.getName() + "  not exists");
				}
		}
		return false;
	}

	private void createTable(MetaTable table) throws SnipManagerException {
		StringBuilder builder = new StringBuilder();
		for (MetaColumn column : table.getColumns()) {
			if (builder.length() > 0) {
				builder.append(SnipUtils.COMMA);
				builder.append(SnipUtils.NL);
			}
			builder.append(snipBuilder.buildMetaColumnClause(column));
		}
		Snip mainSnip = new Snip(null, SnipType.CREATE, MessageFormat.format(SnipUtils.CREATE_TABLE, table.getName().trim(), builder.toString()), null, null, null, -1, null);
		List<Snip> fetchList = new ArrayList<Snip>();
		Set<String> keySet = new HashSet<String>();
		for (MetaPrimaryKey key : table.getPrimaryKeys()) {
			keySet.add(key.PK_NAME);
			String pK_NAME = key.PK_NAME;
			if (pK_NAME.equalsIgnoreCase("PRIMARY")) {
				pK_NAME = "";
			}
			String keyClause = MessageFormat.format(SnipUtils.ALTER_TABLE_PK, table.getName(), pK_NAME, key.COLUMN_NAME);
			fetchList.add(new Snip(null, SnipType.EXECUTE, keyClause, null, null, null, -1, null));
		}
		for (MetaLink link : table.getImportedKeys()) {
			String keyClause = MessageFormat.format(SnipUtils.ALTER_TABLE_ADD, table.getName(), MessageFormat.format(SnipUtils.FOREIGN_KEY, link.FKCOLUMN_NAME, link.PKTABLE_NAME, link.PKCOLUMN_NAME));
			fetchList.add(new Snip(null, SnipType.EXECUTE, keyClause, null, null, null, -1, null));
		}
		Map<String, List<MetaIndex>> indexListMap = new HashMap<String, List<MetaIndex>>();
		for (MetaIndex index : table.getIndexList()) {
			if (keySet.contains(index.INDEX_NAME)) {
				continue;
			}
			List<MetaIndex> list = indexListMap.get(index.INDEX_NAME);
			if (list == null) {
				list = new ArrayList<MetaIndex>();
				indexListMap.put(index.INDEX_NAME, list);
			}
			list.add(index);
		}
		for (Entry<String, List<MetaIndex>> entry : indexListMap.entrySet()) {
			StringBuilder columnBuilder = new StringBuilder();
			for (MetaIndex index : entry.getValue()) {
				if (columnBuilder.length() > 0) {
					columnBuilder.append(SnipUtils.COMMA);
				}
				columnBuilder.append(index.COLUMN_NAME);
			}
			String indexClause = MessageFormat.format(SnipUtils.CREATE_INDEX_COPY, entry.getKey(), table.getName(), columnBuilder.toString());
			fetchList.add(new Snip(null, SnipType.EXECUTE, indexClause, null, null, null, -1, null));
		}
		mainSnip.setFetch(fetchList.toArray(new Snip[fetchList.size()]));
		snipManager.execute(mainSnip);
	}

	private void restoreTableData(String shemaName, MetaTable table) throws SnipManagerException {
		Snip snip = new Snip(null, SnipType.INSERT, MessageFormat.format(
			SnipUtils.INSERT_INTO,
			(shemaName != null && shemaName.length() > 0) ? shemaName + "." + table.getName() : table.getName(),
			getColumnString(table),
			getValuesString(table)), null, null, null, -1, null);
		snip.setData(table.getData().toArray(new Object[table.getData().size()][]));
		snipManager.execute(snip);
		log("    Restored : " + table.getName() + "  rows: " + table.getData().size());
	}

	private void log(String message) {
		Logger.getLogger(this.getClass().getName()).info(message);
	}

	private List<MetaTable> wrapTables(String shemaName, boolean include, String... tableNames) throws SnipManagerException {
		List<MetaTable> tables = snipManager.getTables(shemaName);
		if (tableNames != null && tableNames.length > 0) {
			Arrays.sort(tableNames);
			Iterator<MetaTable> iterator = tables.iterator();
			while (iterator.hasNext()) {
				if (include) {
					if (Arrays.binarySearch(tableNames, iterator.next().getName()) < 0) {
						iterator.remove();
					}
				} else {
					if (Arrays.binarySearch(tableNames, iterator.next().getName()) >= 0) {
						iterator.remove();
					}
				}
			}
		}
		return tables;
	}

	private String getColumnString(MetaTable table) {
		StringBuilder builder = new StringBuilder("");
		for (MetaColumn column : table.getColumns()) {
			if (builder.length() > 0) {
				builder.append(SnipUtils.COMMA);
			}
			builder.append(column.COLUMN_NAME);
		}
		return builder.toString();
	}

	private Object getValuesString(MetaTable table) {
		StringBuilder builder = new StringBuilder("");
		for (int a = 0; a < table.getColumns().size(); a++) {
			if (builder.length() > 0) {
				builder.append(SnipUtils.COMMA);
			}
			builder.append(SnipUtils.QU);
		}
		return builder.toString();
	}

	public ProgressTask getProgressTask() {
		return progressTask;
	}
}
